函数部分比较难的地方在函数指针的各个概念,需要多加注意
函数基础
函数的调用和返回
调用相关
调用运算符:调用运算符的形式是一对圆括号,作用于一个表达式,该表达式是函数或者指向函数的指针。圆括号之内是一个用逗号隔开的实参列表,我们用实参初始化函数的形参。调用运算符的类型就是函数的返回类型
函数调用完成两项工作:
- 使用实参初始化函数对应的形参
- 将控制权转移给被调函数。此时,主调函数的执行被打断,被调函数开始执行
返回相关
一般的类型函数都可以进行返回,当函数不需要返回任何值时,可以返回void,当然也可以返回空语句。
但是函数返回类型不能是数组,但是可以是指向数组或函数的指针,
return语句同样完成两项工作:
- 返回return语句中的值
- 将控制权从被调函数转移回主调函数。
形参与实参
函数有几个参数,就必须提供相同数量的实参,因为参数的调用规定实参数量要和形参一致,所以形参一定会被初始化。
同时,形参的类型一定要被实参很好的满足。比如如果形参是int类型,实参可以是double类型,因为可以隐式转换,但却不能是const char*类型。
函数的形参列表
当一个函数没有形参时,可以书写一个空的形参列表,为了和C语言兼容,函数的形参列表可以用关键字void表示函数没有参数。
偶尔有函数的个别参数不会被使用,则此类形参通常不命名以表示在函数体内不会使用。但即便如此,函数调用时,依然应该为其提供一个实参。
局部对象
在C++中,名字有作用域,对象有生命周期。
- 名字的作用域是程序文本的一部分,名字在其中可见
- 对象的生命周期是程序执行过程中该对象存在的一段时间
形参和函数体内部定义的变量统称为局部变量。局部变量会在外层作用域中同名的其他所有声明里隐藏(意思就是在外层作用域如果存在同名变量,则局部变量是无法访问到的。)
自动对象
函数的控制路径经过变量定义语句时创建的对象,该对象当到达定义所在块的末尾时会进行销毁,只存在于块执行期间的对象,就是自动对象。
- 形参就是一种自动对象。该自动对象在函数开始时申请存储空间,由实参进行初始化,在函数结束时被销毁
- 对于非形参的局部变量的自动对象,如果含有初始值,则使用初始值进行初始化;否则执行默认初始化。也就是说内置类型的未初始化局部变量将产生未定义的值。
局部静态变量
如果要让局部变脸的生命周期贯穿函数调用,及之后的时间,可以将局部变量定义为static类型。
局部静态变量在程序执行路径第一次经过对象定义语句时初始化,直到程序终止才被销毁,在此期间即使对象的所有函数执行也不会对他有影响。
如果局部静态变量没有显式的初始值,则将执行值初始化,内置类型的局部静态变量初始化为0.
函数声明
函数的名字必须在使用前声明,函数只能定义一次,但能声明多次。
函数声明可以省去形参的名字,只要形参的类型。
函数声明也称为函数原型。
函数声明建议放在头文件中而不是源文件中,便于更改。
定义函数的源文件把含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配。
分离式编译
C++支持分离式编译,也就是允许多个源文件共同编译,或各自编译,生成对象文件,再进行链接成可执行文件
参数传递
C++的参数传递分为值传递或引用传递两大类。
值传递
值传递本质就是形参和实参是完全两个不同的变量,形参只是利用实参的值进行拷贝初始化。
而对于指针传参而言,同样依然是一种“值传递”,这种值传递不过是把指针的值进行了拷贝传递,拷贝之后形参的指针和实参的指针依然是两个完全不同的指针,只是他们访问的对象是同一个对象罢了。
以前在C语言中,我们把参数传递分为按值传参和按址传参,在C++中更经常使用引用传参而不是按址传参。
引用传递
引用的本质就是通过给原本的对象起一个别名,然后使用它。对引用的操作实际上是作用在引用的对象上的。引用形参是同一个道理。
最重要的是通过引用传参,可以避免变量的复制。对于大的类型(string等)还有不支持拷贝操作的类型(IO类型),我们要使用引用形参的方式来访问该类型的对象。同时,如果函数无需修改引用类型的值,最好用常量进行引用
引用是一种和指针非常类似的东西,也就是说我们也可以用引用传参的方法,返回额外的信息(因为return只能返回一个值)。
const形参和实参
使用实参初始化形参时,会忽略掉顶层const(顶层const的具体含义见章节二)。也就是说,当形参有顶层的const时,传给他常量对象或者非常量对象都是合法的。
由于C++中虽然允许函数名相同的函数存在,但前提是不同函数的形参列表应该有明显区别。那么如果两个函数的唯一区别就是形参列表中有无顶层const,那么第二个函数就是错误的,因为是重复定义的函数,如下述代码所示:
1 | void fcn(const int i){```/*fcn可以读取i,但不可以改变i*/```} |
我们可以使用非常量初始化一个底层const对象,但是无法用一个底层const对象来初始化一个非const指针;同时一个普通的引用必须用同类型的对象初始化。如下述代码
1 | int i = 42; |
同样这些规则也适合于函数传参。
但是当我们的形参类型是常量引用时,确实可以使用一个字面值作为实参进行初始化。
在函数不会改变形参时尽量使用常量引用
原因是:常量引用可以扩大函数所能接受的实参类型。如上文所述,顶层const可以忽略对于函数引用,不会影响非const类型的接受,但是如果是非const引用,则会导致无法接受const类型参数进行初始化——尽管那可能是我们想要的。
数组形参
数组存在两个性质:不允许拷贝数组、使用数组时通常会将其转换为指针
所以当数组作为形参时,我们无法用值传递的方式使用数组参数,实际上我们是将指向数组首元素的指针传入函数
形如:
1 | void print(const int*); |
都是将数组传入函数的写法,且上述三种表示的含义相同——都表示传入的参数为const int*
类型。编译器检查只会检查是否为这种类型。
和其他使用数组的代码一样,以数组为形参的函数也必须保证数组不越界,管理指针形参有三种常用的技术
- 使用标记指定数组长度。即规定数组中含有某个元素,标记数组的结束。比如C风格字符串,会以空字符作为字符串结束的标志
- 使用标准库规范。即传递数组的首元素和尾后元素的指针。一般可以使用标准库的begin()函数和end()函数
- 显示传递一个数组大小的形参。在C程序和过去的C++程序中常用,调用函数时,提前用一个变量表示函数大小,作为参数传递过去。
数组形参和const
同引用一样,如果函数不需要对数组元素执行写操作,数组形参应该是指向const的指针
数组引用形参
如同之前的引用传参,形参也可以是数组的引用,此时引用形参绑定到对应的实参,也就是绑定到数组上。写法如下所示:
1 | void print(int (&arr)[10]); |
其中&arr
两端的括号必不可少。
传递多维数组
C和C++中没有真正意义的多维数组,只有存放了数组的数组。而一般多维数组的写法如下所示
1 | void print(int (*matric)[10],int rowSize); |
一般来说,数组作为传参,编译器不会在乎你的数组容量,但是会在乎你的数组中元素的类型。正如前面介绍的,传入参数传入的实际只是首元素的地址,而n维函数也不过只是存放了一个(n-1)维数组的一维数组,所以编译器会优化掉你的第一个维度(也就是一维数组的长度),但是却需要知道你的类型。这也就是为什么我们传入指针时,需要标注10,传入数组时,省略第一个括号中的值的原因。
main:处理命令行选项
1 | int main(int argc, char* argv[]){···········} |
其中argc为命令行传入参数数目+1,同时也代表argv数组的大小。
argv是一个存放字符串的数组,其中它的第一个元素必然是可执行文件,后面的元素即为传进来的参数。(书上说最后一个元素一定为0,事实上打印时发现如果尝试打印下个数,会段错误)
含有可变形参的函数
在无法提前预支需要向函数传递多少个参数时,C++11提供了两种主要方法:
- initializer_list标准库类型:要求所有的实参类型都相同
- 可变参数模板:实参类型不同
同时C++还有一种形参类型——省略符,可以用来传递可变数量的实参,一般用于与C函数交互的接口程序
initializer_list
定义在同名头文件需要引入。相关操作如图所示
需要注意的是,initializer_list对象中的元素永远是常量值,无法改变其中元素的值。
如果想向initializer_list形参中传递一个值的序列,则必须把序列放在一对花括号里。
省略符形参
使用了varargs的C标准库功能。
1 | void foo(...); |
返回类型和return类型
关于return的有无
当函数的返回类型不是void,则该函数内每条return语句必须返回一个值。且返回值的类型必须与函数返回类型相同,或可以隐式转换。
同样 在含有return语句的循环后面也要有一条return语句,如果没有,该程序就是错误的。
不要反悔局部对象的引用或指针
形如:
1 | const string &manip() |
这样的程序,两条返回语句都是错误的,第一条语句实际返回的是局部对象的引用,第二条语句实际返回的是局部临时量。
综上,返回局部对象的引用或指针都是错误的, 因为会在函数结束后释放掉空间,指针或引用就访问了不可用的内存空间。
返回类类型的函数和调用运算符
调用运算符优先级和点运算符、箭头运算符相同,且符合左结合律。
引用返回左值
函数的返回类型决定函数调用是否为左值。当函数返回引用时,得到左值,其他返回类型为右值。返回类型为引用的函数可以像其他左值一样进行使用,比如我们可以为返回类型为非常量引用的函数的结果赋值。当然如果为常量引用,我们依旧无法赋值。
列表初始化返回值(C++11)
C++11规定,函数可以返回花括号包围的值的列表。此处列表也是用来对表示函数返回的临时量进行初始化。
- 列表为空时,临时量执行值初始化
- 否则,返回的值由函数的返回类型决定。
如果函数返回的是内置类型,则花括号包围的列表最多包含一个值,而且该值所占空间不能大于目标类型的空间。如果函数返回的是类类型,则需要由类本身定义初始值如何使用。
main主函数
主函数可以不显示的写上return 0,编译器一般会自己隐式插入。
main函数的返回值是状态指示器。返回0表示执行成功,其他值表示失败,具体非0值的含义跟机器相关。为了机器无关,cstdlib头文件定义了两个预处理变量,分别表示成功与失败
1 |
|
main函数不可以调用自己
返回数组指针
定义一个返回数组的指针或引用有下述几种方法:
类型别名
typedef int arrT[10]
。arrt表示了一个类型别名,表示的类型为含有10个整数的数组using arrT=int[10]
。 同上
这样,就有函数arrT* func(int i)
来返回一个指向含有10个整数的数组的指针。其中arrT是含有10个整数的数组的别名。
声明一个返回数组指针的函数
不使用类型别名,就需要用比较繁琐的方式进行表示,函数形式如下所示
其中$Type$表示元素类型,$dimension$表示数组大小,$ (*function(parameter_list))$的括号必须存在,如果不存在,返回的就是指针数组。
尾置返回类型(C++11)
C++11中有一种简化的方法,就是尾置返回类型。这种形式对返回类型比较复杂的函数最有效。
尾置返回类型跟在形参列表后面并以一个$->$开头,同时在返回类型处,放置一个auto,如
1 | auto func(int i) -> int(*) [10]; |
这里返回了一个指针,指针指向含有10个整数的数组。
使用decltype
这应用于我们提前知道了函数返回的指针将要指向哪个数组。如下所示
1 | int odd[] = {1,3,5,7,9}; |
但是需要注意decltype并不负责把数组类型转换成对应的指针,所以decltype的结果为数组,所以应该在arrPtr函数前加上*号。
函数重载
函数名字相同,形参列表不同,称之为重载函数。重载函数允许函数形参数量和形参类型不同,但不允许两个函数除了类型外其他所有要素都相同。
形参类型究竟是否相同
有些函数虽然形参类型看似不同,但是本质不是重载函数。主要指的是类型别名
重载函数和const形参
参考顶层const和底层const](http://rrazz.love/2020/11/04/《C++ Primer 第五版》阅读过程查漏补缺 Chapter2/)),两者要区分开来。
顶层const作为参数时,无法影响传入函数的对象, 也就是无法进行重载。典型代表就是常量指针。
底层const作为参数时,可以理解为不同函数,是一种函数重载,包括常量引用和指向常量的指针。
重载函数和const_cast
第4章介绍了const_cast,作为一种解引用的关键字,在重载函数中很有用。举例假如我们有一个函数shorterString如下所示:
1 | const string &shorterString(const string &s1, const string &s2) |
该函数的参数、返回值都是const string的引用,如果我们对两个非常量 调用该函数,那么显然我们返回的结果是一个const string的引用。这时我们就需要一个重载函数,他要达到的目的是:当我传入实参不是常量时,我得到的结果也应该是一个非常量的引用。重载函数如下所示:
1 | string & shorterString(string &s1, string &s2) |
这样写,最终返回的非常量引用显然是安全的。
函数匹配(重载确定)
调用重载函数可能有三种结果
- 编译器找到一个最佳匹配
- 找不到任何一个函数与调用的实参匹配,编译器发出无匹配错误
- 在多余一个函数可以匹配,但每个都不是最佳选择,此时会发生错误,称为二义性调用
重载和作用域
不同作用域,函数重载不生效。如果在新的子作用域中声明了某一个函数,而和他同名的其他函数未在作用域声明,则其他重载函数会被屏蔽。因为编译器会先从局部作用域中找起,当前作用域找到后就会接受该函数,并忽略外层作用域中的同名实体。
特殊用途语言特性
有三种函数相关的语言特性,分别是默认实参、内联函数和constexpr函数。
默认实参
我们可以为每一个形参提供默认实参,默认实参作为形参初始值出现在形参列表。一旦某个形参被赋予了默认值,形参列表中在他之后的所有形参都要赋予默认值。
tips:对于函数的声明,一般习惯放在头文件,且只声明一次。
局部变量不能作为默认实参。但表达式可以,用作默认实参的名字在函数声明所在的作用域内解析,但是求值过程发生在函数调用
内联函数和constexpr函数
内联函数
内联函数可以避免函数调用的开销。所谓内联函数就是让函数在调用点“内联”展开。
只需要在函数返回值前加上关键字inline,就可以声明为内联函数。
一般来说内联函数用于优化规模小、流程直接、频繁调用的函数
编译器一般不支持内联递归函数以及大于75行的函数。
constexpr函数
指能用于常量表达式的函数。需要遵循下列约定
- 函数的返回值和所有形参的类型都要是字面值类型
- 函数体中必须有且只有一条return语句
同时,constexpr函数一般会被隐式的指定为内联函数。
由于内联函数和constexpr函数可以在程序中多次定义,所以为了保证其多次定义完全一致,他们通常定义在头文件中。
调试帮助
assert预处理宏
assert宏在cassert头文件中定义。预处理名字由预处理器而非编译器管理,所以可以直接使用assert而不是std::assert
assert宏用于检查“不能发生”的条件。本质类似于内联函数。
1 | assert(expr) |
NDEBUG预处理变量
assert的行为依赖于NDEBUG的预处理变量的状态。如果定义了该变量,则assert什么也不做。
函数匹配
重载函数的选用过程就是函数匹配,这一过程主要分为三步:
第一步 是找到重载函数集,也就是候选函数们,候选函数具备两个特征:1. 与被调用的函数同名 2. 它的声明在调用点可见。
第二步 是根据调用提供的实参,确定可行函数。可行函数具备两个特征 1. 形参数量和调用提供的实参数量相同 2. 每个实参的类型和对应形参类型相匹配
在此步骤中,两个小步骤可能存在以下两种特殊情况:
- 具有默认实参的函数比较特殊,在调用该函数时传入的实参数量,可能要少于其实际使用的实参数量。
- 实参形参匹配的含义可能是具有相同的类型,也可能是实参类型和形参类型满足转换规则(比如高精度转低精度)。
第三步 是在可行函数中寻找最匹配的函数,所谓最匹配的基本思想,就是实参和形参类型最接近。详细说来就是两点:
- 最匹配函数的每个实参匹配都不劣于其他可行函数需要的匹配
- 至少有一个实参的匹配,比其他可行函数提供的匹配都优秀。
如果这两点无法满足,则编译器将会报错二义性调用
类型转化的等级排序
- 下述三种情况都属于最优的精确匹配
- 实参与形参类型完全相同
- 实参从数组类型或函数类型转换为指针类型
- 实参添加或删除顶层const
- const转换实现的匹配
- 类型提升实现的匹配
- 算术类型转换、指针转换实现的匹配
- 类类型转换实现的匹配
函数指针
函数指针是指向函数的指针。该函数的类型由返回类型和形参类型有关,和函数名无关
比如:
如有一函数为bool lengthCompare(cosnt string &, const string &)
,则其类型为bool (const string &, const string &)
,指向该类型函数的指针可以声明为bool (*pf) (const string &, const string &)
。其中,*pf
左右的括号必不可少,否则pf只是一个返回值为bool指针的函数,而不是一个指向函数的指针。
使用函数指针
函数指针的使用和其他指针不太相同,主要有以下这些点:
函数名作为右值赋值给指针时,函数可以自动转换为指针,取地址符是可选的
1
2
3// 下面两条语句等价
pf = lengthCompare;
pf = &lengthCompare;指向函数的指针调用该函数时,无需提前解引用指针。
1
2
3
4// 下面三条语句等价
bool b1 = pf("hello","goodbye");
bool b2 = (*pf) ("hello","goodbye");
bool b3 = lengthCompare("hello","goodbye");不同函数类型的指针不存在转换规则,且函数指针可以赋值为nullptr。
对于重载函数的指针,指针类型必须与候选函数中的某一个精准匹配。
函数类型是无法被定义为形参的,但是我们可以使用指向函数的指针,也就是函数指针,来作为函数的形参,此时,形参看上去是函数类型,但其实实际上是被视为指针使用。
1
2
3// 下面两条语句等价
void useBigger(const string &s1, const string &s2, bool pf(const string &, cosnt string &));
void useBigger(const string &s1, const string &s2, bool (*pf) (const string &, const string &));此时我们将函数直接作为实参转入,其会自动转换为指针类型。
上面直接使用函数作为形参,使得代码很长,我们使用类型别名和decltype简化函数指针的代码:
1
2
3
4
5
6// Func和Func2就是函数类型
typedef bool Func(const string &, const string &);
typedef decltype(lengthCompare) Func2;
// FuncP和FuncP2是函数指针
typedef bool (*FuncP) (const string &, const string &);
typedef decltype(lengthCompare) *FuncP2;需要注意,上面和下面是不等价的,decltype返回函数类型,在此时,是不会将函数类型自动转换为指针类型的。所以只能加上*号,才可以得到函数指针。
从而得到简单的写法:
1
2void useBigger(const string &s1, const string &s2,Func);
void useBigger(const string &s1, const string &s2, FuncP2)函数类型无法作为实际的参数,所以在作为形参时,可以自动转换为函数指针,但是函数类型作为返回时,却无法自动转换为函数指针,所以当我们需要返回一个函数指针时,必须显式地将函数返回类型指定为函数指针。使用类型别名可以简单的表示返回的函数指针:
1
2using F = int(int *, int);
using PF = int(*) (int*, int);其中,F为一个函数类型,PF为一个指向函数类型的指针。注意在定义完返回的函数指针后,正确的函数写法分别有以下两种:
1
2F *func1(int);
PF func1(int)前者使用了定义的函数类型,由于无法向形参那样自动转换为函数指针,所以需要显式的加上*号。后者使用了定义的函数指针,所以可以直接接到返回值。
在不使用类型别名时,上面这个返回值为函数指针的函数还可以写为下面的形式:
1
int (*f1(int)) (int *, int);
首先f1具有形参列表,所以f1是一个函数,其次有*号,说明返回了一个指针,然后指针的类型包括了形参列表,所以指针指向函数,被指向的函数的返回类型是int。
或者使用尾置返回类型:
1
auto f1(int) -> int (*) (int*, int);
在明确知道返回函数是谁时,可以使用
decltype
简化上述过程,直接使用deccltype
得到已知的返回的函数类型,由于decltype
返回的是函数类型而非指针,所以要加上*号来返回一个指针1
2string::size_type sumLength(const string&, const string&);
decltype(sumLength) *getFun(const string&);getFunc函数返回的就是指向sumLength函数类型的指针。
原文链接: https://zijian.wang/2021/07/11/《C++ Primer第五版》阅读过程查漏补缺 chapter6/
版权声明: 转载请注明出处.